package cn.yx.common.log.aspect;

import cn.hutool.core.util.ArrayUtil;
import cn.yx.common.core.enums.ResponseCodeEnum;
import cn.yx.common.core.enums.YesNoEnum;
import cn.yx.common.core.enums.http.HttpHeaderCustomEnum;
import cn.yx.common.core.enums.http.HttpHeaderEnum;
import cn.yx.common.core.pojo.ResponseVo;
import cn.yx.common.core.util.date.DateUtil;
import cn.yx.common.core.util.json.jackson.JsonUtil;
import cn.yx.common.core.util.reflect.ReflectUtil;
import cn.yx.common.log.annotation.SysLog;
import cn.yx.common.log.service.SysLogService;
import cn.yx.common.security.util.SecurityUtil;
import cn.yx.common.web.util.IpUtil;
import cn.yx.common.web.util.SpringUtil;
import cn.yx.sys.remote.dto.SysLogRemoteDto;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * <p>SysLog切面类，完成日志切面逻辑，保存日志</p>
 *
 * @author Wgssmart
 */
@Component
@Aspect
@Slf4j
public class SysLogAspect {

    private final SysLogService sysLogService;

    private long startTime;
    private long endTime;
    private MethodSignature signature;
    private SysLog sysLog;

    public SysLogAspect(SysLogService sysLogService) {
        this.sysLogService = sysLogService;
    }

    @Pointcut("@annotation(cn.yx.common.log.annotation.SysLog)")
    public void methodAspect() {
    }

    /**
     * 环绕通知，整合前置通知，返回通知，后置通知，异常通知
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("methodAspect()")
    public Object around(ProceedingJoinPoint joinPoint) {
        // 开始执行时间
        startTime = DateUtil.millis();
        signature = (MethodSignature) joinPoint.getSignature();
        sysLog = signature.getMethod().getDeclaredAnnotation(SysLog.class);
        try {
            Object result = joinPoint.proceed();
            endTime = DateUtil.millis();
            ResponseVo<?> responseVo = getResponseVo(result);
            SysLogRemoteDto sysLogRemoteDto = createSysLogDto(joinPoint, responseVo);
            handleSysLog(sysLogRemoteDto);
            return result;
        } catch (Throwable e) {
            log.error("aspect around joinPoint.processed error", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 从result获取ResponseVo
     *
     * @param result
     * @return
     */
    private ResponseVo<?> getResponseVo(Object result) {
        ResponseVo<?> responseVo = null;
        if (result instanceof ResponseVo) {
            responseVo = (ResponseVo<?>) result;
            responseVo.setTransId(SpringUtil.getServletRequest()
                    .getAttribute(HttpHeaderCustomEnum.TRANSACTION_ID.getName()).toString());
        }
        return responseVo;
    }

    /**
     * 创建SysLogDto
     *
     * @param joinPoint
     * @param responseVo
     * @return
     */
    private SysLogRemoteDto createSysLogDto(JoinPoint joinPoint, ResponseVo<?> responseVo) {
        SysLogRemoteDto sysLogRemoteDto = new SysLogRemoteDto();
        sysLogRemoteDto.setUserId(SecurityUtil.currentUserId());
        sysLogRemoteDto.setDescription(sysLog.description());
        HttpServletRequest request = SpringUtil.getServletRequest();
        sysLogRemoteDto.setRequestIp(IpUtil.getIp(request));
        sysLogRemoteDto.setUserAgent(request.getHeader(HttpHeaderEnum.USER_AGENT.getName()));
        sysLogRemoteDto.setRequestUrl(request.getRequestURL().toString());
        sysLogRemoteDto.setRequestMethod(request.getMethod());
        sysLogRemoteDto.setExecuteClass(signature.getDeclaringTypeName());
        sysLogRemoteDto.setExecuteMethod(signature.getName());
        sysLogRemoteDto.setStartTime(DateUtil.date(startTime));
        sysLogRemoteDto.setEndTime(DateUtil.date(endTime));
        sysLogRemoteDto.setCostTime(endTime - startTime);
        sysLogRemoteDto.setRequestData(sysLog.requestData() ?
                JsonUtil.toJsonStr(getRequestData(joinPoint.getTarget().getClass(), signature.getName(), joinPoint.getArgs()))
                : null);
        if (Objects.nonNull(responseVo)) {
            sysLogRemoteDto.setResponseCode(responseVo.getCode());
            sysLogRemoteDto.setResponseTransId(responseVo.getTransId());
            sysLogRemoteDto.setResponseMessage(responseVo.getMessage());
            sysLogRemoteDto.setResponseData(sysLog.responseData() ? JsonUtil.toJsonStr(responseVo) : null);
            if (responseVo.getCode() != ResponseCodeEnum.OK.getCode().intValue()) {
                // 出现异常
                sysLogRemoteDto.setHasException(YesNoEnum.Y.getValue());
                sysLogRemoteDto.setExceptionMessage(responseVo.getMessage());
            }
        }
        return sysLogRemoteDto;
    }

    /**
     * 处理请求数据，如果有需要排除的属性，则把属性值修改为**hidden**
     *
     * @param clazz
     * @param methodName
     * @param args
     * @return
     */
    private Map<String, Object> getRequestData(Class<?> clazz, String methodName, Object[] args) {
        Map<String, Object> requestData = SpringUtil.getParamValues(clazz, methodName, args);
        if (Objects.isNull(requestData)) {
            return null;
        }
        String[] excludeProperties = sysLog.excludeProperties();
        if (ArrayUtil.isEmpty(excludeProperties)) {
            // 不需要排除属性，直接返回请求数据
            return requestData;
        }
        return rewriteRequestData(requestData);
    }

    /**
     * 重写请求数据，隐藏需要排除的属性
     *
     * @param requestData
     * @return
     */
    private Map<String, Object> rewriteRequestData(Map<String, Object> requestData) {
        String[] excludeProperties = sysLog.excludeProperties();
        // 创建请求数据副本，不影响原始的请求数据
        Map<String, Object> requestDataCopy = new HashMap<>(requestData.size());
        for (Map.Entry<String, Object> entrySet : requestData.entrySet()) {
            Object requestObj = entrySet.getValue();
            // 获取请求对象副本，不影响原始的请求对象
            Object requestObjCopy = JsonUtil.toBean(JsonUtil.toJsonStr(requestObj), requestObj.getClass());
            for (String excludeProperty : excludeProperties) {
                if (ReflectUtil.hasProperty(requestObjCopy, excludeProperty)) {
                    // 修改请求对象副本中的属性值为不可见
                    ReflectUtil.invokeSetter(requestObjCopy, excludeProperty, "******");
                }
            }
            requestDataCopy.put(entrySet.getKey(), requestObjCopy);
        }
        return requestDataCopy;
    }

    /**
     * 处理SysLogDto，如打印日志，异步保存到DB
     *
     * @param sysLogRemoteDto
     */
    private void handleSysLog(SysLogRemoteDto sysLogRemoteDto) {
        // 打印日志
        loggerSysLog(sysLogRemoteDto);
        // 异步存储日志到数据库
        asyncSaveSysLogToDb(sysLogRemoteDto);
    }

    private void loggerSysLog(SysLogRemoteDto sysLogRemoteDto) {
        if (YesNoEnum.Y.getValue().equals(sysLogRemoteDto.getHasException())) {
            // 如果有异常，则打印error日志
            log.error("sysLogDto\n{}", JsonUtil.toPrettyJsonStr(sysLogRemoteDto));
            return;
        }
        if (sysLog.enableLogger()) {
            // 无异常情况下如果启用了logger日志，则打印nfo日志
            log.info("sysLogDto\n{}", JsonUtil.toPrettyJsonStr(sysLogRemoteDto));
        }
    }

    private void asyncSaveSysLogToDb(SysLogRemoteDto sysLogRemoteDto) {
        if (sysLog.saveToDb()) {
            sysLogService.saveAsync(sysLogRemoteDto);
        }
    }

}